京东支付SDK重构设计与实现
SDK
作者:王超
京东科技支付团队原创,转载请获得授权
业务发展太快,早期技术架构已经不能很好的适应变化,而业务需求又繁重,架构升级计划一次次被延后,最后不了了之。
既然架构不能支持新业务,就只能通过各种“旁门左道”的方式破坏架构来解决问题,以至于进化成没有架构,只有各位前辈高人馈遗的祖传套路,谓之“祖宗家法不可变”。
没有实际价值的业务代码一直苟延残喘的留在系统里,变成长期的维护负担。
设计文档、接口文档、代码注释缺失或更新不及时,致使涉及多系统交互的代码后人往往只能因循将就,不敢轻言优化。
一、支付业务组成
常言道:脱离业务的架构都属于自嗨。为实现重构目标,我们需要:
先梳理清楚业务特点,做业务层抽象;
找出当前软件系统痛点所在,做技术层分析;
结合业务层抽象与技术层分析,设计新解决方案;
上图比较宏观的把SDK划分为几大组成单元,特点是:
所有组成单元之间都是双向依赖,任何一个业务单元都可以作为其他业务单元的前置流程,也可以成为其他业务单元的下一步流程,很多业务单元内部还存在互相依赖。而这种循环、交叉的依赖,重构之难可想而知,修改一处影响一片。每当试图把重构拆分成多个小任务来迭代执行时就会发现,粒度实难控制,因为改着改着就涉及上百个文件了… 业务变种众多,举个例子,仅短信验证一个功能就有内单、外单、支付验证、风控加验、白条开通、证书安装、全屏页、半屏页、特殊业务等诸多变种,这些变种彼此组合才能完成一个短信验证操作,如“内单+风控加验+半屏”这几个组合就是一种常见的短信验证流程,而“外单+风控加验+全屏”又是另一种组合,依此类推。
异常流程繁杂,为了尽可能使用户完成支付,必须识别并区别处理各种失败情况。如:忘记密码的要引导用户找回密码、余额不足的要引导用户更换支付方式等等。异常流程往往伴随着多次支付流程重试行为,也就是说已经执行过的流程,部分数据要保留,部分数据要替换,因此,确保模块重新执行时入参和出参的精准性也是一大难题。
二、经典架构模式能否解决问题?
这是网上流传很广泛的一张图,View和Presenter无需多说,Router负责模块(页面)跳转,而Entity和Interactor大体上是把传统的Model职责拆开,纯数据对象作为Entity(Bean),Interactor用来管理调度数据。但是,问题在于怎么来管理数据?我们考虑有两种可能:
1、将Interactor设计为Presenter级别数据管理器
这样的话,那么支付这种模块众多且交叉、循环耦合的业务,谁来处理模块间数据流转的准确性呢?如图所示,Interactor与Router并没有直接交互,而是通过Presenter来处理。这就使得单个模块的Presener可能需要知道其他模块所需的数据来自哪里,以及如何组装出下个模块的入参,如此一来,Presenter难免感知、耦合其他模块。当一个模块耦合了一堆其他模块之时,牵一发动全身就不难理解了。不幸的是,京东支付SDK重构前就存在这种情况,各种验证工具模块更是重灾区,因为几乎每种验证工具的Presenter中都包含了一堆业务场景的定制逻辑。举个例子:
密码验证Presenter由A、B、C业务调用时的入参、出参各不相同,下一步流程也不一样,这种情况下如果Router的数据由密码验证Presenter来提供的话,势必要耦合前后各种不同的业务逻辑。那么,如果给每种业务场景提供专属Presenter怎么样呢?支付SDK重构前也是这么做的,仅短信验证至少就有8种对接不同业务的Presenter实现,然而并不能彻底解决问题,因为每种验证方式都可能衔接N种后续流程,所以在短信验证Presenter里构建Router数据还是免不了把其他流程的逻辑乱入进来。这也是多年以来一直困扰支付SDK的一大问题:让一个模块只做自己这一件事儿,太难了。
2、将Interactor设计为全局数据管理器
三、Scene与Interactor,DDD设计实践
Scene是整个业务流的核心,类似于DDD中领域层,管理并调度影响主干流程的所有数据,它与UI无关,但它任何时候都可以根据所持有的数据知道当前业务流执行到哪一步了,以及下一步要做什么、需要哪些数据。
Interactor是Scene的辅助,与VIPER中Interactor定位不同,它定位为DDD中UI层与应用层的结合,面向业务场景(即用例),负责处理业务流上用户主动触发的关键交互事件(如:页面之间的跳转、需要其他模块协作的),并交由Scene来处理业务逻辑,再把结果反馈给用户。
如图所示,Current Business Unit即当前正在执行任务的模块,假设它是密码验证模块,交互如下:
用户输入密码后,该模块将输入数据封装成一个Event事件发出来;
Interactor识别并接收这个Event,把它交给Scene中处理密码输入的方法进行处理;
Scene的密码处理方法去调用服务端接口验证密码;
验证失败,把错误信息封装成Event发出来,密码模块接收并处理;
验证成功,Scene根据持有的流程数据判断下一步做什么,并将数据组装好,交给Interactor;
Interactor收到Scene处理后的数据,完成模块跳转。
四、UserCase
new UserCase()
.business(createBusinessA(), JPPRuntime.getAsyncWorker())
.business(createBusinessB(), JPPRuntime.getMainWorkder())
.business(createBusinessC(), JPPRuntime.getAsyncWorker())
.business(createBusinessD(), JPPRuntime.getMainWorkder())
.execute(new Observer() {
@Override
public void onComplete(@NonNull UserCase userCase) {
}
@Override
public void onError(@NonNull Throwable throwable) {
}
});
运行时可以回溯业务流调用链,轻松知道用户操作过程。
某个业务单元出错,可以快速地回退到上一个正确的业务单元上重新执行,给用户以最小代价重试的机会,而不必从头重来。
对于存在业务流循环调用的场景,不必为循环额外做什么,UserCase支持重定向到任意业务单元上继续顺序执行,使实现A->B->C->B->C->D这种业务流成为很简单的事儿。
public interface Business<I, O> {
int getId();
I getInput();
void setInput(@Nullable I input);
O getOutput();
void onExecute(@NonNull UserCase userCase, @Nullable Business prev);
}
定向跳转时UserCase通过ID在业务链上查找业务单元。
业务单元执行的入参(Input)由外部传入,所以允许set,而执行后的出参(Output)则是只读的,这样每次业务单元执行后的入参、出参就可以形成一份数据快照,UserCase回溯流程时便有迹可循。为保证每个业务单元数据快照的稳定性,避免引用型入参、出参被外部修改的问题,我们还开发了一个数据深拷贝工具,实现一行代码复制任何对象(包括对象内所有层级的子对象)。
五、业务模版
重构以后,支付SDK每个业务场景都有一个特定的Scene、Interactor和众多业务单元,如图:
每个BusinessUnit都实现了Business接口,其中内聚了该业务相关的入参、出参和ID;
BusinessScene和BusinessInteractor是配对关系,彼此互相引用紧密协作;
BusinessScene集成了特定业务场景所需的所有BusinessUnit(如:密码验证、收银台、绑卡等模块);
BusinessInteractor在createUserCase()时,从BusinessScene中获取这些BusinessUnit并编排业务链,生成该业务的UserCase;
onEvent()接收并处理各BusinessUnit与用户交互过程中需要BusinessScene/BusinessInteractor配合的事件,如:需要验证密码时,当前BusinessUnit发出请求验证密码事件,BusinessInteractor接收到以后请求BusinessScene根据当前流程状态决定展示何种密码验证页,BusinessScene把结果(密码验证页入参)告知BusinessInteractor,并由BusinessInteractor启动密码验证页;
六、京东支付SDK新架构
如前文所述,此次重构专注于重组SDK业务逻辑,使新架构能更好的支持业务需求迭代,提升开发效率。总结起来如下:
首先,根据业务流来重新组织代码,每个业务流就是一套Scene+Interactor+UserCase的组合,可以理解为一个业务沙箱,沙箱内是完整的业务运行时环境,不支持的功能,不会存在于沙箱中,也就不会在运行时意外乱入,而整个业务流由Scene+Interactor+UserCase组合来决策;
其次,业务单元Widget化,只做自己本职工作,绝不插手业务流程;
再次,充分利用事件驱动模型来解耦业务单元间的依赖关系,承担全局消息总线职责;
最后,为了满足宿主App对SDK功能、体积的要求,重构后把非标业务或功能做了成动态模块,通过Gradle在编译时一键配置是否集成进SDK中。动态模块另外一个好处是,可以支持定制化需求,又不必深度入侵标准业务。
七、重构收益
我们以同一版本京东App为宿主,分别把新、老两个SDK集成进去,在相同入口用相同订单测试:
1、启动时长对比
启动时长指:从京东支付SDK主Activity启动到第一个接收用户交互的Fragment响应onResume()生命周期这段时间,其间包含了一次后端接口调动,但多次测试使用的参数是一样的。
重构前 | 重构后 | |
第一次时长(ms) | 6619 | 3549 |
第二次时长(ms) | 7809 | 4265 |
平均(ms) | 7214 | 3907 |
2、纯业务(Java)代码量对比
重构前 | 重构后 | |
代码总行数 | 75778 | 35820 |
文件个数 | 604 | 355 |
总大小(kB) | 3574 | 1686 |
单个最大(kB) | 155 | 101 |
3、资源文件(XML)对比
重构前 | 重构后 | |
代码总行数 | 14204 | 7688 |
文件个数 | 238 | 143 |
总大小(kB) | 681 | 398 |
单个最大(kB) | 38 | 30 |
重构前 | 重构后 | |
文件个数 | 238 | 143 |
总大小(kB) | 681 | 398 |
单个最大(kB) | 38 | 30 |
往期好文推荐:
> Apache顶级项目ShardingSphere — SQL Parser的设计与实现
> 16篇论文入选AAAI 2021,京东数科AI都在关注什么?(附论文下载)